/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2022-2022. All rights reserved.
 *
 */

import fsExtra from 'fs-extra';
import fsp from 'fs/promises';
import path from 'path';

import { NativeCommandBuilder } from '../builder/native-command-builder.js';
import { ModulePathInfoIml } from '../common/iml/module-path-info-iml.js';
import { NativeConst } from '../const/common-const.js';
import { CpuAbiEnum } from '../enum/cpu-abi-enum.js';
import { NativeLibraryModel } from '../model/cxx/native-library-model.js';
import { FileUtil } from '../utils/file-util.js';
import { OhosLogger } from '../utils/log/ohos-logger.js';

import { TaskNames } from './common/task-names.js';
import { ModuleTargetData } from './data/hap-task-target-data.js';
import { TargetTaskService } from './service/target-task-service.js';
import { BuildNativeWithCmake } from './build-native-with-cmake.js';
import Task = TaskNames.Task;
import { DurationEvent, MetricLogType, PathUtil } from '@ohos/hvigor';

import { CmakeUtil } from '../utils/cmake-util.js';

import { AbstractBuildNative } from './abstract-build-native.js';

/**
 * ohos native代码编译so任务
 *
 * @since 2021/1/21
 */
export class BuildNativeWithNinja extends AbstractBuildNative {
  private _log: OhosLogger = OhosLogger.getLogger(BuildNativeWithNinja.name);

  constructor(taskService: TargetTaskService) {
    super(taskService, Task.BUILD_NATIVE_WITH_NINJA);
  }

  initTaskDepends() {
    this.declareDepends(BuildNativeWithCmake.name);
    for (const moduleName of this.service.getDependencyInfo().getModuleDependencyMap().keys()) {
      const service =
        this.projectModel.getTarget(moduleName, this.targetName) ??
        this.projectModel.getTarget(moduleName, 'default') ??
        this.projectModel.getTarget(moduleName);
      const target = service?.getTargetData().getTargetName() ?? 'default';
      this.declareDepends(`${target}@${Task.DO_NATIVE_STRIP.name}`, moduleName);
    }
  }

  async buildCommand(abiFilter: string, targetData: ModuleTargetData, callback: Function, callbackInput: unknown[], subDurationEvent: DurationEvent) {
    const pathInfo = targetData.getPathInfo();
    const output = path.resolve(pathInfo.getNinjaWorkDir(), abiFilter);
    const builder = new NativeCommandBuilder(this.sdkInfo.getNativeNinjaTool()).changeToDir(output);
    const commands = builder.build();
    if (this._nativeOption?.targets && this._nativeOption.targets.length > 0) {
      commands.push(...this._nativeOption.targets);
    }
    this._log._printDebugCommand('Ninja', commands);

    subDurationEvent.stop();
    subDurationEvent.setLog(subDurationEvent.getName(), MetricLogType.INFO);
    const submitTaskName = `submit CMake task to work pool with ABI of ${abiFilter}`;
    const subToolDurationEvent = this.durationEvent.createSubEvent(submitTaskName, '');
    subToolDurationEvent.start();

    await this.executeCommand(commands, output, callback, callbackInput, subToolDurationEvent);

    subToolDurationEvent.stop();
    subToolDurationEvent.setLog(submitTaskName, MetricLogType.INFO);
  }

  taskShouldDo(): boolean {
    const shouldDo = CmakeUtil.nativeTaskCondition(this.moduleModel, this.targetData, this._nativeOption);
    if (!shouldDo) {
      fsExtra.emptyDirSync(this.pathInfo.getIntermediatesCppOutPut());
    }
    return shouldDo;
  }

  protected async doTaskAction(): Promise<void> {
    const pathInfo: ModulePathInfoIml = this.targetData.getPathInfo();
    const outputDir = path.resolve(pathInfo.getIntermediatesCppOutPut());

    const abiFilters = CmakeUtil.checkAbiFilters(this._nativeOption?.abiFilters, this.targetData.isHarmonyOS(), this.moduleModel, this.targetName);
    await this.cleanUnwanted(abiFilters);
    for (const abiFilter of abiFilters) {
      const subTaskName = `generate Ninja command with ABI of ${abiFilter}`;
      const subDurationEvent = this.durationEvent.createSubEvent(subTaskName, '');
      subDurationEvent.start();

      const expectedOutput = path.resolve(outputDir, abiFilter);
      const cxxAbi = path.resolve(pathInfo.getNinjaWorkDir(), abiFilter);
      await this.buildCommand(
        abiFilter,
        this.targetData,
        async () => {
          const nativeLibraries = CmakeUtil.parseLibraries(cxxAbi, this.targetName, abiFilter);
          await this.syncOutput(nativeLibraries, expectedOutput, cxxAbi);
        },
        [],
        subDurationEvent,
      );
      await this.syncStl(abiFilter, expectedOutput, cxxAbi, pathInfo);
    }
  }

  async cleanUnwanted(abiFilters: string[]) {
    if (!fsExtra.existsSync(this.pathInfo.getIntermediatesCppOutPut())) {
      return;
    }
    const dirs = await fsp.readdir(this.pathInfo.getIntermediatesCppOutPut(), { withFileTypes: true });
    for (const dir of dirs) {
      if (dir.isDirectory() && !abiFilters.includes(dir.name)) {
        fsExtra.rmdirSync(path.resolve(this.pathInfo.getIntermediatesCppOutPut(), dir.name), { recursive: true });
      }
    }
  }

  /**
   * 检查.so实际输出与默认输出并同步
   *
   * @param nativeLibraries NativeLibraryModel
   * @param expectedOutput 默认输出
   * @param cxxAbi .cxx中间目录
   * @private
   */
  private async syncOutput(nativeLibraries: Map<string, NativeLibraryModel>, expectedOutput: string, cxxAbi: string) {
    // 同步的so缓存, 避免重复拷贝
    const index = new Map<string, string>();
    const targets: string[] | undefined = this._nativeOption?.targets;
    for (const [name, library] of nativeLibraries.entries()) {
      if (targets && targets.length > 0 && targets.indexOf(name) === -1) {
        // 当在externalNativeOptions指定targets, 只同步声明的targets
        this._log.debug(`Case targets are configured, dependency ${name} are not synchronized.`);
        continue;
      }
      await this.syncLibOutputs(library, expectedOutput, cxxAbi, index);
    }
  }

  /**
   * 同步本模块生成的so产物到intermediates/cmake目录下
   *
   * @param library NativeLibraryModel
   * @param expectedOutput 默认输出
   * @param cxxAbi .cxx中间目录
   * @param index 全局同步的so缓存, 避免重复拷贝
   * @private
   */
  private async syncLibOutputs(library: NativeLibraryModel, expectedOutput: string, cxxAbi: string, index: Map<string, string>) {
    const nameOnDisk = library.getNameOnDisk();
    if (!nameOnDisk) {
      return;
    }
    const expectedLibPath = path.resolve(expectedOutput, nameOnDisk);
    const outputs = library
      .getOutputs()
      .map((output) => (path.isAbsolute(output) ? path.resolve(output) : path.resolve(cxxAbi, output))) // 计算so路径
      .filter((output) => fsExtra.existsSync(output) && !PathUtil.checkCopyPathIsSame(output, expectedLibPath)); // 判断so存在
    for (const output of outputs) {
      this._log.debug(`External build set its own library output location for ${nameOnDisk}, which will be hard linked or copy to the expected location.`);
      await FileUtil.linkFile(output, expectedLibPath);
    }

    // 同步library的依赖so
    await this.syncLibDeps(library, expectedOutput, cxxAbi, index);
  }

  /**
   * 同步本模块生成的so的依赖so到intermediates/cmake目录下
   *
   * @param library NativeLibraryModel
   * @param expectedOutput 默认输出
   * @param cxxAbi .cxx中间目录
   * @param index 全局同步的so缓存, 避免重复拷贝
   * @private
   */
  private async syncLibDeps(library: NativeLibraryModel, expectedOutput: string, cxxAbi: string, index: Map<string, string>) {
    const ohSdkDir = path.normalize(this.sdkInfo.getSdkNativeDir());
    const hmsSdkDir = this.targetData.isHarmonyOS() ? path.normalize(this.sdkInfo.getHmsNativeDir()) : undefined;
    for (let file of library.getRuntimeFiles()) {
      file = path.isAbsolute(file) ? path.resolve(file) : path.resolve(cxxAbi, file);
      file = path.normalize(file);
      this._log.debug(`Sync native runtime file ${file}.`);
      const destination = path.resolve(expectedOutput, path.basename(file));
      if (!fsExtra.existsSync(file) || PathUtil.checkCopyPathIsSame(file, destination) || this.excludeFromHsp(file)) {
        await this.removeHarOutPutSo(file, destination);
        continue;
      }
      if (this.isExcludeFromHar(file)) {
        this._log.debug(`Exclude from har: ${file}`);
        continue;
      }
      if (index.get(file) && index.get(file) === destination) {
        continue;
      }
      if ((hmsSdkDir && file.startsWith(hmsSdkDir)) || file.startsWith(ohSdkDir)) {
        index.set(file, destination);
        continue;
      }
      index.set(file, destination);
      await FileUtil.linkFile(file, destination);
    }
  }

  /**
   * 是否需要exclude
   * @param file
   * @private
   */
  private isExcludeFromHar(file: string): boolean {
    const { excludeFromHar = true } = this.targetService.getNativeLibOption();
    return this.moduleModel.isHarModule() && excludeFromHar && this.excludeFromHar(file);
  }

  /**
   * 如果该so文件已存在于缓存中，并且需要exclude，需将缓存中的so文件删除
   * @param file
   * @param destination
   * @private
   */
  private async removeHarOutPutSo(file: string, destination: string) {
    if (fsExtra.existsSync(file) && fsExtra.existsSync(destination) && this.isExcludeFromHar(file)) {
      fsExtra.rmSync(destination);
      this._log.debug(`Remove the so cache file: ${destination}`);
    }
  }

  /**
   * 同步标准库stl
   *
   * 设置了 -DOHOS_STL=c++_static 或者 -DOHOS_STL=none 时，不拷贝 libc++_shared.so，且删除临时目录下的.so文件
   * 否则拷贝libc++_shared.so到临时目录
   * 最终打包到hap的lib目录下
   *
   * @param abiFilter abiFilter
   * @param outputDir 临时目录
   * @param replyFolder cmake file-api reply folder
   * @param pathInfo pathInfo
   * @private copyLibSo
   */
  private async syncStl(abiFilter: string, outputDir: string, replyFolder: string, pathInfo: ModulePathInfoIml): Promise<void> {
    const stl = CmakeUtil.parseStlFromCMakeCache(replyFolder);
    const delStl = stl.value === 'c++_static' || stl.value === 'none';
    const libSo = path.resolve(outputDir, 'libc++.so');
    const sharedSo = path.resolve(outputDir, NativeConst.SHARED_STL_LIBRARY);
    const libsSo = path.resolve(pathInfo.getModuleBuildIntermediates(), 'libs', abiFilter);
    this._log.debug(`Copy STL ${NativeConst.SHARED_STL_LIBRARY} to ${outputDir} due to OHOS_STL is 'c++_shared'`);
    if (delStl) {
      this._log.debug(`${this.moduleName} remove files start`);
      fsExtra.removeSync(libSo);
      fsExtra.removeSync(sharedSo);
      fsExtra.removeSync(libsSo);
      this._log.debug(`${this.moduleName} remove files end`);
      return;
    }
    const soLibPath = path.resolve(this.sdkInfo.getSdkNativeDir(), 'llvm', 'lib', CpuAbiEnum.getCpuType(abiFilter), 'c++');
    const sharedLibSo = path.resolve(soLibPath, NativeConst.SHARED_STL_LIBRARY);
    if (!fsExtra.existsSync(sharedLibSo)) {
      this._log.warn(`Missing stl library ${sharedLibSo};`);
      return;
    }
    const stats = fsExtra.statSync(sharedLibSo);
    if (fsExtra.existsSync(sharedSo)) {
      const existStats = fsExtra.statSync(sharedSo);
      if (stats.size === existStats.size && stats.mtimeMs === existStats.mtimeMs) {
        return;
      }
    }
    await fsExtra.copy(sharedLibSo, sharedSo, { overwrite: true, recursive: true });
  }

  private excludeFromHsp(libPath: string) {
    const localHsps = this.service
      .getDependencyInfo()
      .getHspDependencies()
      .map((pkg) => pkg.getDependencyRootPath());

    return !!localHsps.find((modulePath) => path.normalize(libPath).startsWith(path.normalize(modulePath)));
  }

  private excludeFromHar(libPath: string) {
    libPath = path.normalize(fsExtra.realpathSync(libPath));
    return !!this.service.getHarDependencies().find((dependency) => {
      const dependencyRootPath = path.normalize(dependency.getDependencyRootPath());
      const relativePath = path.relative(dependencyRootPath, libPath);
      return !relativePath.startsWith('..') && !path.isAbsolute(relativePath);
    });
  }
}
